#!/bin/sh -x
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# /init script for use in recovery installer.  Note that this script uses the
# busybox shell (not bash, not dash).

. /lib/init.sh

# USB card partition and mount point.
USB_DEVS="sdb3 sdc3 mmcblk1p3"
USB_MNT=/usb
REAL_USB_DEV=
DM_NAME=

STATEFUL_MNT=/stateful
STATE_DEV=

LOG_DEV=
LOG_DIR=/log
LOG_FILE=${LOG_DIR}/recovery.log
TAIL_PID=

TPM_B_LOCKED=
TPM_PP_LOCKED=

REAL_KERN_B_HASH=

# Special file systems required in addition to the root file system.
BASE_MOUNTS="/sys /proc /dev"

# Used to ensure the factory check only occurs with
# a properly matched root and kernel.
UNOFFICIAL_ROOT=0

. /lib/completion_settings.sh
. /lib/messages.sh

# Load default settings, which may be overridden by board_recovery.sh.
. /lib/defaults.sh
if [ -f /lib/board_recovery.sh ]; then
  . /lib/board_recovery.sh
fi

# Look for a device with our GPT ID.
wait_for_gpt_root() {
  [ -z "$KERN_ARG_KERN_GUID" ] && return 1
  dlog -n "Looking for rootfs using kern_guid..."
  for try in $(seq 20); do
    dlogf " ."
    kern=$(cgpt find -1 -u $KERN_ARG_KERN_GUID)
    # We always try ROOT-A in recovery.
    newroot="${kern%[0-9]*}3"
    if [ -b "$newroot" ]; then
      USB_DEV="$newroot"
      dlog "Found $USB_DEV"
      return 0
    fi
    sleep 1
  done
  dlog "Failed waiting for kern_guid"
  return 1
}

# Look for any USB device.
wait_for_root() {
  dlog -n "Waiting for $USB_DEVS to appear"
  for try in $(seq 20); do
    dlogf " ."
    for dev in $USB_DEVS; do
      if [ -b "/dev/$dev" ]; then
        USB_DEV="/dev/$dev"
        dlog "Found $USB_DEV"
        return 0
      fi
    done
    sleep 1
  done
  dlog "Failed waiting for root!"
  return 1
}

check_if_dm_root() {
  dump_kernel_config "$KERN_B_DEV" | grep -q 'root=/dev/dm-' || return 1
  return 0
}

# Attempt to find the root defined in the signed recovery
# kernel we're booted into to. Exports REAL_USB_DEV if there
# is a root partition that may be used - on succes or failure.
find_official_root() {
  dlogf "Checking for an official recovery image . . ."

  # Check for a kernel selected root device or one in a well known location.
  wait_for_gpt_root || wait_for_root || return 1

  # Now see if it has a Chrome OS rootfs partition.
  cgpt find -t rootfs "$(strip_partition "$USB_DEV")" || return 1
  REAL_USB_DEV="$USB_DEV"

  # USB_DEV points to the rootfs partition of removable media. And its value
  # can be one of /dev/sda3 (arm), /dev/sdb3 (x86, arm) and /dev/mmcblk1p3
  # (arm). Get stateful partition by replacing partition number with "1".
  LOG_DEV="${USB_DEV%[0-9]*}"1  # Default to stateful.

  # Must verify that install kernel hash matches current kernel hash argument.
  verify_install_kernel_hash || return 1

  # Now see if the root should be integrity checked.
  if check_if_dm_root; then
    setup_dm_root || return 1
  fi

  mount_usb || return 1
  return 0
}

find_developer_root() {
  is_developer_mode || return 1
  # Lock the TPM prior to using an untrusted root.
  lock_tpm || return 1
  dlogf "\nSearching for developer root . . ."
  # If an official root could not be mounted, free up the underlying device
  # if it is claimed by verity.
  dmsetup remove "$DM_NAME"

  # If we found a valid rootfs earlier, then we're done.
  # TODO(wad) Attempt to setup an unofficial dm root prior to
  # mounting the USB directly
  USB_DEV="$REAL_USB_DEV"
  [ -z "$USB_DEV" ] && return 1
  set_unofficial_root || return 1
  mount_usb || return 1
  return 0
}

is_old_style_verity_argv() {
  # TODO(ellyjones): remove by 2011-08-31. Part of crosbug.com/15772.
  # "0 1740800 verity %U+1 %U+1 1740800 0 sha1 $hash"
  local depth=$(echo "$1" | cut -f7 -d' ')
  if [ "$depth" = "0" ]; then
    return 0
  fi
  return 1
}

get_kern_b_device() {
  # TODO(wad) By changing boot priority, we could end up checking the recovery
  # image or the recovery image could not be in slot A. In that case, it should
  # fail in normal mode.
  KERN_B_DEV=${REAL_USB_DEV%[0-9]*}4
  if [ ! -b "$KERN_B_DEV" ]; then
    return 1
  fi
  return 0
}

get_real_kern_b_hash() {
  REAL_KERN_B_HASH=$(dd if="$KERN_B_DEV" | sha1sum | cut -f1 -d' ')
  [ -n "$REAL_KERN_B_HASH" ]
}

verify_install_kernel_hash() {
  get_kern_b_device || return 1
  get_real_kern_b_hash || return 1

  # TODO(wad) Check signatures from stateful on kern-b using the root of trust
  # instead of using a baked in cmdline.
  if [ "$REAL_KERN_B_HASH" != "$KERN_ARG_KERN_B_HASH" ]; then
    if ! is_developer_mode; then
      dlog "The recovery kernel cannot be verified."
      return 1
    fi
  fi

  return 0
}

# parse_dm_table is passed the dm argment from the kernel command
# line from the image and builds a table for dmsetup that has just
# the information needed to bring up verity so the image can be
# verified before it is installed. This is only done if verity was
# setup in image. The boot cache is ignored.
#
# BNF for device mapper (dm) argument syntax:
# In the future, the <num> field will be mandatory.
# TODO(taysom:defect 32847)
#
# <device>        ::= [<num>] <device-mapper>+
# <device-mapper> ::= <head> "," <target>+
# <head>          ::= <name> <uuid> <mode> [<num>]
# <target>        ::= <start> <length> <type> <options> ","
# <mode>          ::= "ro" | "rw"
# <uuid>          ::= xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | "none"
# <type>          ::= "verity" | "bootcache" | ...
#
# Specific case of arguments for boot cache and verity:
# $1   2          Num of devices
# $2   vboot      Name of boot cache device
# $3   none       uuid
# $4   ro         Read-only device
# $5   1,0        Num entries and start (one argument because no space)
# $6   1768000    End of device presented to layer above
# $7   bootcache  Device mapper code to use for device
# $8   5e560a5b-15f5-924a-85c8-84c67b07ee99+1    uuid of underlying device
# $9   1768000    Start of boot cache data
# $10  cf20e4499efb35b8a3dedbf3e84a6a55750003e5  Salt for verity
# $11  512        Max sectors requested that will be cached
# $12  20000      Max trace events that will be kept
# $13  100000,    Max pages that will be cached
# $14  vroot      Name of verity device
# $15  none       uuid
# $16  ro         Read-only device
# $17  1,0        Num entries and start (Treated as a single argument)
# $18  1740800    End of device presented to layer above
# $19  verity payload=254:0
# $20  hashtree=254:0
# $21  hashstart=1740800
# $22  alg=sha1
# $23  root_hexdigest=cf20e4499efb35b8a3dedbf3e84a6a55750003e5
# $24  salt=20ba9113fe2c46f38393bbe630126f87378ff01bdcbf87384929bfe43f9e56ce
#
parse_dm_table() {
  case $1 in
    2)  # Both bootcache and verity in the new dm-init format
      local uuid="$8"
      local vroot="${*##*vroot}"
      local table="${vroot##*,}"
      local first="${table%%payload*}"
      local last="${table##*hashstart}"
      local table="${first}payload=${uuid} hashtree=${uuid} hashstart${last}"
      ;;
    1|vroot) # Just verity in both old and new dm-init format
      local vroot="${*##*vroot}"
      local table="${vroot##*,}"
      ;;
    *) dlog "Unexpected argument to parse_dm_table:$1"
      local table=
      ;;
  esac
  # We override the reboot-to-recovery error behavior so that we can fail
  # gracefully on invalid rootfs.
  if is_old_style_verity_argv "$table"; then
    local eio=eio
  else
    local eio='error_behavior=eio'
  fi
  echo "$table $eio"
}

setup_dm_root() {
  local eio
  dlog -n "Extracting the device mapper configuration..."
  # export_args can't handle dm="..." at present.
  # We have to substitute the GUID of the recovery kernel for the %U variable.
  DMARG=$(dump_kernel_config "$KERN_B_DEV" |
    sed -e 's/.*dm="\([^"]*\)".*/\1/g;t;d' |
    sed -e "s/%U/$KERN_ARG_KERN_GUID/g")

  # Make sure we have valid dm args string.
  if [ -z "$DMARG" ]; then
    dlog "Failed to extract dm arguments from kernel command line"
    return 1
  fi

  DM_NAME=vroot
  DM_TABLE=$(parse_dm_table $DMARG)

  # Don't attempt to call dmsetup if the root device isn't one that was
  # discovered as the creation process will hang.
  # TODO(wad) extract the UUID and use it with cgpt find instead.
  if [ -n "$KERN_ARG_KERN_GUID" ]; then
    [ "${DM_TABLE%$KERN_ARG_KERN_GUID*}" = "$DM_TABLE" ] && return 1
  elif [ -n "$USB_DEV" ]; then
    [ "${DM_TABLE%$USB_DEV*}" = "$DM_TABLE" ] && return 1
  fi

  if ! dmsetup create -r "$DM_NAME" --major 254 --minor 0 --table "$DM_TABLE"
  then
    dlog "Failed to configure device mapper root"
    return 1
  fi
  USB_DEV="/dev/dm-0"
  if [ ! -b "$USB_DEV" ]; then
    mknod -m 0600 "$USB_DEV" b 254 0
  fi
  dlog "Created device mapper root $DM_NAME."
  return 0
}

mount_usb() {
  dlog -n "Mounting usb"
  for try in $(seq 20); do
    dlogf " ."
    if mount -n -o ro "$USB_DEV" "$USB_MNT"; then
      dlog "ok"
      return 0
    fi
    sleep 1
  done
  dlog "Failed to mount usb!"
  return 1
}

get_stateful_dev() {
  STATE_DEV=${REAL_USB_DEV%[0-9]*}1
  if [ ! -b "$STATE_DEV" ]; then
    dlog "Failed to determine stateful device"
    return 1
  fi
  return 0
}

unmount_usb() {
  dlog "Unmounting $USB_MNT"
  umount "$USB_MNT"
  # Make sure we clean up a device-mapper root.
  if [ "$USB_DEV" = "/dev/dm-0" ]; then
    dlog "Removing dm-verity target"
    dmsetup remove "$DM_NAME"
  fi
  dlog
  dlog "$REAL_USB_DEV can now be safely removed"
  dlog
  return 0
}

strip_partition() {
  local dev="${1%[0-9]*}"
  # handle mmcblk0p case as well
  echo "${dev%p*}"
}

# Usage: save_log_files [log_dev] [log_fs]
# Save log files stored in LOG_DIR in addition to demsg to the device specified.
# Args:
#  log_dev: The block device holding the filesystem where the logs should be
#      copied to. By default the LOG_DEV device is used, which points to the
#      stateful partition in the USB_DEV device.
#  log_fs: The filesystem type (default: ext4).
save_log_files() {
  # The recovery stateful is usually too small for ext3.
  # TODO(wad) We could also just write the data raw if needed.
  #           Should this also try to save
  local log_dev="${1:-$LOG_DEV}"
  local log_fs="${2:-ext4}"

  [ -z "${log_dev}" ] && return 0

  if [ ! -b "${log_dev}" ]; then
    dlog "Can't store logs on passed device '${log_dev}': not a block device."
    return 1
  fi

  dlog "Dumping dmesg to ${LOG_DIR}"
  dmesg > "${LOG_DIR}"/dmesg

  # TODO(sosa): Remove once caller scripts recover all of the log dir. See
  # crbug.com/213731 and crbug.com/212794 for details.
  cat "${LOG_DIR}"/dmesg >> "${LOG_FILE}"

  dlog "Saving log files from: ${LOG_DIR} -> ${log_dev}"
  err=0
  [ ${err} -ne 0 ] ||
    mount -n -t "${log_fs}" -o sync,rw "${log_dev}" /tmp || err=1
  [ ${err} -ne 0 ] || rm -rf /tmp/recovery_logs || err=1
  [ ${err} -ne 0 ] || mkdir -p /tmp/recovery_logs || err=1
  [ ${err} -ne 0 ] || cp "${LOG_DIR}"/* /tmp/recovery_logs || err=1
  # Attempt umount, even if there was an error to avoid leaking the mount.
  umount -n /tmp || err=1

  if [ ${err} -eq 0 ] ; then
    dlog "Successfully saved the log file"
  else
    dlog "Failures seen trying to save log file"
  fi
}

stop_log_file() {
  # Drop logging
  exec >"${TTY_DEBUG}" 2>&1
  [ -n "$TAIL_PID" ] && kill $TAIL_PID
}

is_unofficial_root() {
  [ $UNOFFICIAL_ROOT -eq 1 ]
}

set_unofficial_root() {
  UNOFFICIAL_ROOT=1
  return 0
}

is_nonchrome() {
  crossystem "mainfw_type?nonchrome" || crossystem "mainfw_type?netboot"
}

is_developer_mode() {
  # Legacy/unsupported systems are mapped to developer mode.
  is_nonchrome && return 0
  # Otherwise the exit status will be accurate.
  crossystem "devsw_boot?1"
}

lock_tpm() {
  if [ -z "$TPM_B_LOCKED" ]; then
    # Depending on the system, the tpm may need to be started.
    # Don't fail if it doesn't work though.
    tpmc startup >/dev/null 2>&1
    tpmc ctest
    if ! tpmc block; then
      if is_nonchrome; then
        dlog "No security chip appears to exist in this non-Chrome device."
        dlog "The security of your experience will suffer."
        # Forge onward.
      else
        dlog "An unrecoverable error occurred with your security device"
        dlog "Please power down and try again."
        dlog "Failed to lock bGlobalLock."
        on_error
        return 1  # Never reached.
      fi
    fi
    TPM_B_LOCKED=y
  fi
  if [ -z "$TPM_PP_LOCKED" ]; then
    # TODO: tpmc pplock if appropriate
    TPM_PP_LOCKED=y
  fi
  return 0
}

# Extract and export kernel arguments
export_args() {
  # We trust our kernel command line explicitly.
  local arg=
  local key=
  local val=
  local acceptable_set='[A-Za-z0-9]_'
  for arg in "$@"; do
    key=$(echo "${arg%%=*}" | tr 'a-z' 'A-Z' | \
                   tr -dc "$acceptable_set" '_')
    val="${arg#*=}"
    export "KERN_ARG_$key"="$val"
    dlog "Exporting kernel argument $key as KERN_ARG_$key"
  done
}

main() {
  exec >"${LOG_FILE}" 2>&1

  initialize

  # Send all verbose output to debug tty.
  (tail -f "${LOG_FILE}" > "${TTY_DEBUG}") &
  TAIL_PID=$!

  # Export the kernel command line as a parsed blob prepending KERN_ARG_ to each
  # argument.
  export_args $(cat /proc/cmdline | sed -e 's/"[^"]*"/DROPPED/g')

  message startup

  if ! find_official_root ; then
    if find_developer_root; then
      message developer_image
    else
      on_error
    fi
  fi

  # Extract the real boot source, which may be masked by dm-verity.
  get_stateful_dev || on_error

  . /lib/recovery_init.sh
  recovery_install
}

# Make this source-able for testing.
if [ "$0" = "/init" ]; then
  main "$@"
  # Should never reach here.
  exit 1
fi
